package store.aixx.jwt.demo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.MessageDigestPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import store.aixx.jwt.demo.common.IErrorStatus;
import store.aixx.jwt.demo.common.R;
import store.aixx.jwt.demo.common.ResponseErrorStatusEnum;
import store.aixx.jwt.demo.constant.AuthConstant;
import store.aixx.jwt.demo.domain.vo.Auth;
import store.aixx.jwt.demo.entity.User;
import store.aixx.jwt.demo.security.filter.JwtFilter;
import store.aixx.jwt.demo.security.filter.MyAuthenticationFilter;
import store.aixx.jwt.demo.security.service.MyUserDetailsService;
import store.aixx.jwt.demo.util.JwtUtil;

import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * @author yukai
 * @since 2021年12月10日 16:17
 */
@Slf4j
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtFilter jwtFilter;

    private final MyUserDetailsService myUserDetailsService;

    private final MessageSource messageSource;

    private final JwtUtil jwtUtil;

    private final ObjectMapper objectMapper;


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(myUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterAt(myAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests(
                        req -> req
                                .antMatchers("/hello/**", "/authentication/**").permitAll()
                                .anyRequest().authenticated()
                )
                //
                .exceptionHandling(
                        ex -> ex
                                // 授权异常
                                .accessDeniedHandler((req, res, accessDeniedException) -> {
                                    res.setStatus(HttpStatus.FORBIDDEN.value());
                                    res.setContentType(MediaType.APPLICATION_JSON_VALUE);
                                    res.setCharacterEncoding(StandardCharsets.UTF_8.name());
                                    res.getWriter().println(objectMapper.writeValueAsString(R.error(ResponseErrorStatusEnum.FORBIDDEN)));
                                })
                                // 认证异常
                                .authenticationEntryPoint((req, res, authEx) -> {
                                    res.setStatus(HttpStatus.UNAUTHORIZED.value());
                                    res.setContentType(MediaType.APPLICATION_JSON_VALUE);
                                    res.setCharacterEncoding(StandardCharsets.UTF_8.name());
                                    if (StringUtils.hasText(req.getHeader(AuthConstant.TOKEN_HEADER_NAME))){
                                        res.getWriter().println(objectMapper.writeValueAsString(R.error(ResponseErrorStatusEnum.TOKEN_CAN_NOT_USE)));
                                    }else{
                                        res.getWriter().println(objectMapper.writeValueAsString(R.error(ResponseErrorStatusEnum.NEED_LOGIN)));
                                    }
                                }))
                .formLogin().and()
                .cors(cors -> cors.configurationSource(new CorsConfigurationSource() {
                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                        CorsConfiguration corsConfiguration = new CorsConfiguration();

                        return corsConfiguration;
                    }
                }))
                .csrf(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
        ;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        // 默认编码算法的 id, 新的密码编码都会使用这个 id 对应的编码格式
        String idForEncoder = "bcrypt";
        // 要支持的多种编码
        Map<String, PasswordEncoder> passwordEncoderMap = new HashMap<>();
        passwordEncoderMap.put(idForEncoder, new BCryptPasswordEncoder());
        passwordEncoderMap.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
        return new DelegatingPasswordEncoder(idForEncoder, passwordEncoderMap);
    }

    public Filter myAuthenticationFilter() throws Exception {
        MyAuthenticationFilter myAuthenticationFilter = new MyAuthenticationFilter();
        myAuthenticationFilter.setUsernameParameter("username");
        myAuthenticationFilter.setPasswordParameter("password");
        myAuthenticationFilter.setAuthenticationManager(this.authenticationManager());
        myAuthenticationFilter.setFilterProcessesUrl("/authentication/token");
        myAuthenticationFilter.setMessageSource(messageSource);
        myAuthenticationFilter.setAuthenticationFailureHandler((req, res, authErr) -> {
            IErrorStatus responseErrorStatus = null;
            if (authErr instanceof AccountExpiredException){
                responseErrorStatus = ResponseErrorStatusEnum.LOGIN_FAILURE_EXPIRED;
            }else if(authErr instanceof LockedException){
                responseErrorStatus = ResponseErrorStatusEnum.LOGIN_FAILURE_ACCOUNT_LOCK;
            }else if(authErr instanceof DisabledException){
                responseErrorStatus = ResponseErrorStatusEnum.LOGIN_FAILURE_ACCOUNT_DISABLE;
            }else if(authErr instanceof CredentialsExpiredException){
                responseErrorStatus = ResponseErrorStatusEnum.LOGIN_FAILURE_PASSWORD_EXPIRED;
            }else{
                responseErrorStatus = ResponseErrorStatusEnum.LOGIN_FAILURE;
            }
            res.setStatus(HttpStatus.UNAUTHORIZED.value());
            res.setContentType(MediaType.APPLICATION_JSON_VALUE);
            res.setCharacterEncoding(StandardCharsets.UTF_8.name());
            res.getWriter().println(objectMapper.writeValueAsString(R.error(responseErrorStatus)));
        });
        myAuthenticationFilter.setTokenHandler((req, res, authentication) -> {
            res.setContentType(MediaType.APPLICATION_JSON_VALUE);
            res.setCharacterEncoding(StandardCharsets.UTF_8.name());
            User principal = (User) authentication.getPrincipal();
            Auth auth = new Auth(
                    jwtUtil.createAccessToken(principal),
                    jwtUtil.createRefreshToken(principal)
            );
            res.getWriter().println(objectMapper.writeValueAsString(R.Success("login.success", auth, null)));
        });
        return myAuthenticationFilter;
    }
}
